#include <memory.h>
#include <stdint.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

#include <openssl/aes.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/md5.h>
#include <openssl/ossl_typ.h>
#include <openssl/ssl.h>

#include "json/json.h"

const std::string aesKey = "qwertyuioplkjhgf";
const std::string aesIv = "zxcvbnmlkjhgfdsa";
const std::string configName = "ota_config.json";
const std::string otaFileName = "ota.tar";
/*
 * openssl aes 128 ecb pkcs5padding 加密
 */
std::string aes_128_cbc_pkcs7_encrypt(const std::string& source, const std::string& key, const std::string& iv) {
    EVP_CIPHER_CTX* ctx;
    ctx = EVP_CIPHER_CTX_new();
    int ret = EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (const unsigned char*)key.data(),
                                 (const unsigned char*)iv.data());
    if (ret != 1)
        throw std::runtime_error("EVP_EncryptInit_ex failed");

    unsigned char* result = new unsigned char[source.length() + 16 - (source.length() % 16)];
    int len1 = 0;
    ret = EVP_EncryptUpdate(ctx, result, &len1, (const unsigned char*)source.data(), source.length());
    if (ret != 1)
        throw std::runtime_error("EVP_EncryptUpdate failed");

    int len2 = 0;
    ret = EVP_EncryptFinal_ex(ctx, result + len1, &len2);
    if (ret != 1)
        throw std::runtime_error("EVP_EncryptFinal_ex failed");

    ret = EVP_CIPHER_CTX_cleanup(ctx);
    if (ret != 1)
        throw std::runtime_error("EVP_CIPHER_CTX_cleanup failed");

    EVP_CIPHER_CTX_free(ctx);
    std::string res((char*)result, len1 + len2);
    delete[] result;
    return res;
}

/*
 * openssl aes 128 ecb pkcs5padding 解密
 */
std::string aes_128_cbc_pkcs7_decrypt(const std::string& ciphertext, const std::string& key, const std::string& iv) {
    EVP_CIPHER_CTX* ctx;
    ctx = EVP_CIPHER_CTX_new();
    int ret = EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (const unsigned char*)key.data(),
                                 (const unsigned char*)iv.data());
    if (ret != 1)
        throw std::runtime_error("EVP_DecryptInit_ex failed");

    unsigned char* result = new unsigned char[ciphertext.length() + 16 - (ciphertext.length() % 16)];
    int len1 = 0;
    ret = EVP_DecryptUpdate(ctx, result, &len1, (const unsigned char*)ciphertext.data(), ciphertext.length());
    if (ret != 1)
        throw std::runtime_error("EVP_DecryptUpdate failed");

    int len2 = 0;
    ret = EVP_DecryptFinal_ex(ctx, result + len1, &len2);
    if (ret != 1)
        throw std::runtime_error("EVP_DecryptFinal_ex failed");

    ret = EVP_CIPHER_CTX_cleanup(ctx);
    if (ret != 1)
        throw std::runtime_error("EVP_CIPHER_CTX_cleanup failed");

    EVP_CIPHER_CTX_free(ctx);
    std::string res((char*)result, len1 + len2);
    delete[] result;
    return res;
}

int get_file_md5(const std::string& file_name, std::string& md5_value) {
    md5_value.clear();

    std::ifstream file(file_name.c_str(), std::ifstream::binary);
    if (!file) {
        return -1;
    }

    MD5_CTX md5Context;
    MD5_Init(&md5Context);

    char buf[1024 * 16];
    while (file.good()) {
        file.read(buf, sizeof(buf));
        MD5_Update(&md5Context, buf, file.gcount());
    }

    unsigned char result[MD5_DIGEST_LENGTH];
    MD5_Final(result, &md5Context);

    char hex[35];
    memset(hex, 0, sizeof(hex));
    for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
        sprintf(hex + i * 2, "%02x", result[i]);
    }
    hex[32] = '\0';
    md5_value = std::string(hex);

    return 0;
}

std::string base64_encodestring(const std::string& text) {
    EVP_ENCODE_CTX* ctx;
    ctx = EVP_ENCODE_CTX_new();
    int size = text.size() * 2;
    size = size > 64 ? size : 64;
    unsigned char* out = (unsigned char*)malloc(size);
    int outlen = 0;
    int tlen = 0;
    EVP_EncodeInit(ctx);
    EVP_EncodeUpdate(ctx, out, &outlen, (const unsigned char*)text.c_str(), text.size());
    tlen += outlen;
    EVP_EncodeFinal(ctx, out + tlen, &outlen);
    tlen += outlen;

    std::string str((char*)out, tlen);
    free(out);
    EVP_ENCODE_CTX_free(ctx);

    return str;
}

std::string base64_decodestring(const std::string& text) {
    EVP_ENCODE_CTX* ctx;
    ctx = EVP_ENCODE_CTX_new();
    unsigned char* out = (unsigned char*)malloc(text.size());
    int outlen = 0;
    int tlen = 0;

    EVP_DecodeInit(ctx);
    EVP_DecodeUpdate(ctx, out, &outlen, (const unsigned char*)text.c_str(), text.size());
    tlen += outlen;
    EVP_DecodeFinal(ctx, out + tlen, &outlen);
    tlen += outlen;

    std::string data((char*)out, tlen);
    free(out);
    EVP_ENCODE_CTX_free(ctx);
    return data;
}

std::string HexToStr(const std::string& str) {
    std::string result;
    for (size_t i = 0; i < str.length(); i += 2) {
        std::string byte = str.substr(i, 2);
        char chr = (char)(int)strtol(byte.c_str(), NULL, 16);
        result.push_back(chr);
    }
    return result;
}

std::string StringToHex(const std::string& data) {
    const std::string hex = "0123456789ABCDEF";
    std::stringstream ss;

    for (std::string::size_type i = 0; i < data.size(); ++i)
        ss << hex[(unsigned char)data[i] >> 4] << hex[(unsigned char)data[i] & 0xf];
    // std::cout << ss.str() << std::endl;
    return ss.str();
}
bool isFileExist(std::string fileName) {
    std::ifstream infile(fileName);
    if (!infile.is_open()) {
        return false;
    } else {
        infile.close();
        return true;
    }
}

int writeInfoJson(const std::string& name,
                  const std::string& path,
                  const std::string& signature,
                  const std::string& start_method) {
    Json::Value root;
    root["name"] = name.c_str();
    root["path"] = path.c_str();
    root["signature"] = signature.c_str();
    root["start_method"] = start_method.c_str();
    Json::StyledWriter sw;

    std::ofstream os;
    os.open(configName, std::ios::out);
    if (!os.is_open())
        return -1;
    os << sw.write(root);
    os.close();

    std::cout << sw.write(root) << std::endl;
    return 0;
}

int packaging(std::vector<std::string>& cmdSet) {
    int ret = -1;
    std::string outputFilePath = cmdSet[0];
    std::string cmdstr = "tar cf ";
    for (auto cmd : cmdSet) {
        cmdstr = cmdstr + cmd + " ";
    }
    std::cout << "cmdstr : " << cmdstr << std::endl;

    ret = system(cmdstr.c_str());
    if (0 == ret) {
        std::cout << "OK : tar file" << std::endl;

    } else {
        std::cout << "ERROR : tar file error" << std::endl;
        return ret;
    }

    std::string md5_str;
    ret = get_file_md5(outputFilePath, md5_str);
    if (0 == ret) {
        std::cout << "OK : get file md5 is " << md5_str << std::endl;
    } else {
        std::cout << "ERROR : get file md5 error" << std::endl;
        return ret;
    }

    std::string aes_str = aes_128_cbc_pkcs7_encrypt(md5_str, aesKey, aesIv);
    std::string base64_str = base64_encodestring(aes_str);
    if (!base64_str.empty()) {
        std::cout << "OK : get aes base64 str is " << base64_str << std::endl;
    } else {
        std::cout << "ERROR : get aes base64 str error" << std::endl;
        return ret;
    }

    ret = writeInfoJson(outputFilePath, "zhangyuhu", base64_str, "reboot");
    if (0 == ret) {
        std::cout << "OK :writeInfoJson " << std::endl;
    } else {
        std::cout << "ERROR : writeInfoJson error" << std::endl;
        return ret;
    }

    std::string cmdStrTarAll = "tar cf ";

    cmdStrTarAll = cmdStrTarAll + otaFileName + " " + outputFilePath + " " + configName;
    std::cout << "cmdStrTarAll : " << cmdStrTarAll << std::endl;

    ret = system(cmdStrTarAll.c_str());
    if (0 == ret) {
        std::cout << "OK : tar file" << std::endl;

    } else {
        std::cout << "ERROR : tar file error" << std::endl;
        return ret;
    }
}

int main(int argc, char* argv[]) {
    std::vector<std::string> cmdSet;

    for (int i = 1; i < argc; i++)
        cmdSet.push_back(argv[i]);

    std::cout << "output : "
              << " " << cmdSet[0] << std::endl;

    for (int i = 1; i < cmdSet.size() - 1; i++) {
        if (isFileExist(cmdSet[i])) {
            std::cout << "input : "
                      << " " << cmdSet[i] << std::endl;
        } else {
            std::cout << "ERROR : File Not Exist" << std::endl;
            return -1;
        }
    }

    packaging(cmdSet);
    return 0;
}